package com.yifeng.repo.controller.excel.loader;

import com.gomcarter.frameworks.base.exception.CustomException;
import com.yifeng.repo.base.utils.common.BaseUtil;
import com.yifeng.repo.controller.excel.configure.ExcelLoaderProperties;
import com.yifeng.repo.controller.excel.loader.biz.BizExportAction;
import com.yifeng.repo.controller.excel.loader.biz.BizImportAction;
import com.yifeng.repo.controller.excel.loader.dto.DataTaskDto;
import com.yifeng.repo.controller.excel.loader.dto.DataTaskTypeEnum;
import com.yifeng.repo.controller.excel.loader.redis.ExcelLoaderRedisWorker;
import com.yifeng.repo.controller.excel.loader.task.DataExportTask;
import com.yifeng.repo.controller.excel.loader.task.DataImportTask;
import com.yifeng.repo.controller.excel.loader.task.handler.ExcelFileHandler;
import com.yifeng.repo.tookit.oss.OssFileClient;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.util.Date;
import java.util.List;

/**
 * Created by daibing on 2022/12/29.
 */
@Slf4j
public class ExcelLoaderManager {
    private final ExcelLoaderProperties properties;
    private final ExcelLoaderRedisWorker redisWorker;
    private final OssFileClient ossFileClient;

    public ExcelLoaderManager(ExcelLoaderProperties properties, ExcelLoaderRedisWorker redisWorker, OssFileClient ossFileClient) {
        this.properties = properties;
        this.redisWorker = redisWorker;
        this.ossFileClient = ossFileClient;
    }

    /**
     * excel-loader组件核心逻辑是简化控制逻辑，就是做减法：
     * 1、导出excel获取源数据需要保存筛选条件才可以做到无状态，当前觉得成本较大，引起代码复杂度增加
     * 2、当前所有导入&导出任务都需要绑定在初始化的节点上执行完成，节点重启后需要重新触发导入&导出操作
     * 3、引入组件后如果没有启动导入&导出动作就不起线程，避免给业务项目带来负担，当然带来的问题就是不支持任务排队，当前通过任务并发数量限制来控制。
     */

    public void init() {
        log.info("yfcloud controller excel-loader init success: OWNER_ID={}, OWNER_NAME={}", BaseUtil.OWNER_ID, BaseUtil.OWNER_NAME);
    }

    public void destroy() {
        log.info("yfcloud controller excel-loader destroy success: OWNER_ID={}, OWNER_NAME={}", BaseUtil.OWNER_ID, BaseUtil.OWNER_NAME);
    }

    /**
     * 列出数据任务: H5 -> excel loader controller -> redis
     */
    public List<DataTaskDto> listDataTask() {
        return redisWorker.listDataTask();
    }

    /**
     * 查询数据任务: H5 -> excel loader controller -> redis
     *
     * @param requestKey 请求唯一值
     */
    public DataTaskDto getDataTask(String requestKey, DataTaskTypeEnum type) {
        return redisWorker.getDataTask(requestKey, type);
    }

    /**
     * 清除数据任务
     *
     * @param prevDays 当前日期几天前的任务将被清除
     */
    public int clearDataTask(int prevDays) {
        List<DataTaskDto> clearDataTasks = redisWorker.clearDataTask(prevDays);
        // 清理临时文件
        for (DataTaskDto clearDataTask : clearDataTasks) {
            new File(clearDataTask.getDataDumpFilePath()).delete();
            switch (clearDataTask.getType()) {
                case EXPORT:
                    ExcelFileHandler.getTemplateFile(clearDataTask.getDataDumpFilePath()).delete();
                case IMPORT:
                    ExcelFileHandler.getOkLogFile(clearDataTask.getDataDumpFilePath()).delete();
                    ExcelFileHandler.getErrorLogFile(clearDataTask.getDataDumpFilePath()).delete();
            }
        }
        return clearDataTasks.size();
    }

    /**
     * 做excel导入: H5 -> Biz backend server -> excel loader controller
     *
     * @param requestKey      请求唯一值，比如导入文件名+操作用户名+时间区间地板值的md5值
     *                        long periodFloorBound = System.currentTimeMillis() / (5 * 60 * 1000L)
     * @param bizImportAction 导入处理器
     */
    public DataTaskDto doImport(String requestKey, BizImportAction bizImportAction) {
        // 1. 检查redis：存在该任务并且任务运行完成或者有效执行中直接返回数据任务
        DataTaskDto dataTaskDto = redisWorker.getDataTask(requestKey, DataTaskTypeEnum.IMPORT);
        if (dataTaskDto != null && this.isValidTask(dataTaskDto)) {
            return dataTaskDto;
        }

        // 2. 检查redis不存在该任务，或者任务已经中断了，都需要先抢占锁确保唯一执行任务
        boolean locked = redisWorker.lock(requestKey, BaseUtil.OWNER_ID.toString(), 2 * 60 * 1000L);
        if (!locked) {
            throw new CustomException(requestKey + "存在并发请求重复发起导入！");
        }

        // 3. 抢占redis后再检查一次redis：检查发现存在该任务属于最新写入的任务，可以直接返回数据任务
        dataTaskDto = redisWorker.getDataTask(requestKey, DataTaskTypeEnum.IMPORT);
        if (dataTaskDto != null && this.isValidTask(dataTaskDto)) {
            return dataTaskDto;
        }

        // 4. 抢占锁成功后，检查该应用当前正在运行中任务数量，未超过限额就启动新任务
        int runningSize = redisWorker.runningDataTaskSize();
        if (runningSize >= properties.getRunningLimitNumber()) {
            throw new CustomException(String.format("%s 任务需要稍候再试，当前正在运行中的任务数量%s 已经超出该应用限额 %s",
                    requestKey, runningSize, properties.getRunningLimitNumber()));
        }

        // 5. 抢占锁成功后，开始启动新任务, 并且写入redis
        dataTaskDto = this.buildDataTask(dataTaskDto, requestKey, DataTaskTypeEnum.IMPORT, bizImportAction.dataFileUrl());
        new DataImportTask(dataTaskDto, bizImportAction, bizImportAction.getTaskConf(properties), redisWorker, ossFileClient).startup();
        String content = redisWorker.saveDataTask(dataTaskDto);
        log.info("import task {} create ok, content={}", requestKey, content);
        return dataTaskDto;
    }

    /**
     * 做excel导出: H5 -> Biz backend server -> excel loader controller
     *
     * @param requestKey      请求唯一值，比如导出筛选参数的md5值
     * @param bizExportAction 导出处理器
     */
    public DataTaskDto doExport(String requestKey, BizExportAction bizExportAction) {
        // 1. 检查redis：存在该任务并且任务运行完成或者有效执行中直接返回数据任务
        DataTaskDto dataTaskDto = redisWorker.getDataTask(requestKey, DataTaskTypeEnum.EXPORT);
        if (dataTaskDto != null && this.isValidTask(dataTaskDto)) {
            return dataTaskDto;
        }

        // 2. 检查redis不存在该任务，或者任务已经中断了，都需要先抢占锁确保唯一执行任务
        boolean locked = redisWorker.lock(requestKey, BaseUtil.OWNER_ID.toString(), 2 * 60 * 1000L);
        if (!locked) {
            throw new CustomException(requestKey + "存在并发请求重复发起导出！");
        }

        // 3. 抢占redis后再检查一次redis：检查发现存在该任务属于最新写入的任务，可以直接返回数据任务
        dataTaskDto = redisWorker.getDataTask(requestKey, DataTaskTypeEnum.EXPORT);
        if (dataTaskDto != null && this.isValidTask(dataTaskDto)) {
            return dataTaskDto;
        }

        // 4. 抢占锁成功后，检查该应用当前正在运行中任务数量，未超过限额就启动新任务，注意这里不是精确控制，并发请求的时候会超出配额。
        int runningSize = redisWorker.runningDataTaskSize();
        if (runningSize >= properties.getRunningLimitNumber()) {
            throw new CustomException(String.format("%s 任务需要稍候再试，当前正在运行中的任务数量%s 已经超出该应用限额 %s",
                    requestKey, runningSize, properties.getRunningLimitNumber()));
        }

        // 5. 抢占锁成功后，开始启动新任务, 并且写入redis
        dataTaskDto = this.buildDataTask(dataTaskDto, requestKey, DataTaskTypeEnum.EXPORT, bizExportAction.templateUrl());
        new DataExportTask(dataTaskDto, bizExportAction, bizExportAction.getTaskConf(properties), redisWorker, ossFileClient).startup();
        String content = redisWorker.saveDataTask(dataTaskDto);
        log.info("export task {} create ok, content={}", requestKey, content);
        return dataTaskDto;
    }

    private boolean isValidTask(DataTaskDto dataTaskDto) {
        // 任务已经完成
        if (dataTaskDto.isTotalFinished()) {
            return true;
        }
        // 任务在刷新周期内，也就是有效执行中
        return dataTaskDto.getRefreshTime().getTime() + properties.getLiveProbeIntervalMillis() >= System.currentTimeMillis();
    }

    /**
     * 构造数据任务对象: 已经在redis存在的任务就直接基于redis数据来更新信息,不存在redis的任务需要全新构建
     */
    private DataTaskDto buildDataTask(DataTaskDto dataTaskDto, String requestKey, DataTaskTypeEnum type, String dataFileUrl) {
        Date now = new Date();
        // 构造数据任务对象, 新任务需要先设置基本信息字段.
        if (dataTaskDto == null) {
            dataTaskDto = new DataTaskDto();
            dataTaskDto.setRequestKey(requestKey);
            dataTaskDto.setType(type);
            dataTaskDto.setDataFileUrl(dataFileUrl);
            dataTaskDto.setDataDumpFilePath(ExcelFileHandler.getDataDumpFilePath(requestKey, type));
            dataTaskDto.setFinishedOffsetId("0");
            dataTaskDto.setCreateTime(now);
        }
        // 构造数据任务对象, 中断的任务仅需要更新owner相关字段.
        dataTaskDto.setOwnerId(BaseUtil.OWNER_ID);
        dataTaskDto.setOwnerName(BaseUtil.OWNER_NAME);
        dataTaskDto.setOwnerTime(now);
        dataTaskDto.setRefreshTime(now);
        dataTaskDto.setModifyTime(now);
        return dataTaskDto;
    }

}
